package jadeCW;

import jade.core.AID;
import jade.core.Agent;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPANames;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jadeCW.behaviour.AllocateAppointment;
import jadeCW.behaviour.RespondToProposal2;
import jadeCW.behaviour.RespondToQuery;
import jadeCW.behaviour.UpdateAppointments;

import java.util.HashMap;
import java.util.Map;

/**
 * Hospital agent class
 * 
 * @author br08, dac108, dv08
 */
public class HospitalAgent extends Agent {
	/**
	 * Generated serialisation ID
	 */
	private static final long serialVersionUID = -3425436450872121494L;

	/**
	 * Name of the service exposed by the hospital
	 */
	public static String SERVICE_NAME = "hospital";
	/**
	 * Type description of the AllocateAppointment service
	 */
	public static String SERVICE_TYPE = "allocate-appointments";
	/**
	 * Key for the total appointments property of the AllocateAppointment
	 * service
	 */
	public static String SERVICE_PROPERTY_TOTAL = "total-appointments";

	/**
	 * Map of allocated appointments to Agent IDs
	 */
	private final Map<Integer, AID> appointments = new HashMap<Integer, AID>();
	/**
	 * Map of requested swaps from patients, where we only have one side of the
	 * request so far
	 */
	private final Map<AID, Integer> swapRequests = new HashMap<AID, Integer>();

	/**
	 * The total number of appointments the hospital can allocate
	 */
	private int totalAppointments;

	/**
	 * When two patients are swapping appointments, checks whether one of the
	 * agents has requested the swap already
	 * 
	 * @param requester
	 *            The ID of the agent to check for a swap
	 * @param appointment
	 *            The appointment the agent wants to swap to
	 * @return If the agent has requested that swap
	 */
	public boolean isSwapRequest(AID requester, Integer appointment) {
		// checks the requester has requested a swap, and if so, that it is for
		// the given appointment
		return (this.swapRequests.containsKey(requester) && this.swapRequests
				.get(requester).equals(appointment));
	}

	/**
	 * Records a request from an agent to swap appointments until the swap is
	 * confirmed
	 * 
	 * @param requester
	 *            The ID of the agent requesting the swap
	 * @param appointment
	 *            The appointment the agent would prefer
	 */
	public void addSwapRequest(AID requester, Integer appointment) {
		this.swapRequests.put(requester, appointment);
	}

	/**
	 * Removes a swap request once both sides have confirmed the swap
	 * 
	 * @param requester
	 *            The ID of the agent requesting the swap
	 * @param appointment
	 *            The appointment the agent would prefer
	 */
	public void removeSwapRequest(AID requester, Integer appointment) {
		// checks the preferred appointment is the one already recorded
		if (this.swapRequests.get(requester) == appointment) {
			this.swapRequests.remove(requester);
		}
	}

	/**
	 * Gets the possible appointments and the agent IDs associated with them
	 * 
	 * @return A map of appointment numbers to agent IDs. Agent ID is null is no
	 *         agent has been assigned to that appointment
	 */
	public Map<Integer, AID> getAppointments() {
		return this.appointments;
	}

	/**
	 * Allocates the given agent the specified appointment
	 * 
	 * @param appointment
	 *            The appointment to allocated
	 * @param id
	 *            The ID of the agent to allocate it to
	 */
	public void setAppointment(int appointment, AID id) {
		this.appointments.put(appointment, id);
	}

	/**
	 * Gets the agent allocated to the given appointment.
	 * 
	 * @param appointment
	 *            The appointment to check
	 * @return The ID of the agent if the appointment is allocated, the ID of
	 *         the hospital if it is free, or null if it is not a valid
	 *         appointment
	 */
	public AID getOwner(int appointment) {
		// if the appointment is not valid
		if (appointment < 0 || appointment > this.totalAppointments) {
			return null;
		}

		// if the appointment has not been allocated
		if (this.appointments.get(appointment) == null) {
			return this.getAID();
		}

		return this.appointments.get(appointment);
	}

	/**
	 * Gets the appointment allocated to the given agent
	 * 
	 * @param owner
	 *            The ID of the agent to get the appointment for
	 * @return The appointment number if the agent has been allocated an
	 *         appointment, -1 otherwise
	 */
	public int getAppointment(AID owner) {
		for (int appointment : this.appointments.keySet()) {
			// if this appointment has not been allocated, ignore
			if (this.appointments.get(appointment) == null) {
				continue;
			}
			// if this allocated appointment is owned by the given agent, return
			// it
			if (this.appointments.get(appointment).equals(owner)) {
				return appointment;
			}
		}
		// no allocated appointment
		return -1;
	}

	@Override
	protected void setup() {
		Object[] args = getArguments();
		if (args != null && args.length > 0) {
			try {
				// get the first argument as the total number of appointments
				totalAppointments = Integer.parseInt((String) args[0]);

				// initialise the appointment map with no allocated agents
				for (int i = 1; i < this.totalAppointments + 1; i++) {
					this.appointments.put(i, null);
				}
			} catch (NumberFormatException e) {
				throw new IllegalArgumentException(
						"Please enter a valid integer as the number of appointments");
			}
		} else {
			throw new IllegalArgumentException(
					"Please provide one arguement for the number of appointments");
		}

		try {
			// create the new agent description, using the hospital's ID
			DFAgentDescription dfd = new DFAgentDescription();
			dfd.setName(getAID());

			// create a service description for the AllocateAppointments service
			ServiceDescription sd = new ServiceDescription();
			sd.setName(SERVICE_NAME);
			sd.setType(SERVICE_TYPE);
			sd.addLanguages(FIPANames.ContentLanguage.FIPA_SL);

			// add the service description to the agent description
			dfd.addServices(sd);

			// register the agent description
			DFService.register(this, dfd);
		} catch (FIPAException fe) {
			fe.printStackTrace();
		}

		// add the hospital behaviours
		addBehaviour(new AllocateAppointment());
		addBehaviour(new RespondToQuery());
		addBehaviour(new RespondToProposal2());
		addBehaviour(new UpdateAppointments());
	}

	@Override
	protected void takeDown() {
		AID patient;
		String patientName;

		// for each valid appointment
		for (int appointment : this.appointments.keySet()) {
			patientName = null;
			patient = this.appointments.get(appointment);

			// if the appointment has an allocated agent, get the agent's name
			if (patient != null) {
				patientName = patient.getLocalName();
			}

			System.out.println(this.getLocalName() + ": Appointment "
					+ appointment + ": " + patientName);
			System.out.println();
		}
	}
}
